package edu.ysu.itrace;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.io.IOException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConditionalExpression;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
/**
* Keeps updated information about the source code viewed in one StyledText.
*/
public class AstManager {
/**
* Types of source code entities.
*/
public enum SCEType {
TYPE,
METHOD,
VARIABLE,
COMMENT,
ENUM,
IMPORT,
FORSTATEMENT,
METHOD_INVOCATION,
IFSTATEMENT,
WHILESTATEMENT,
SWITCHSTATEMENT,
CONDITIONAL_EXPRESSION,
}
public enum SCEHow {
DECLARE,
USE,
LINE_COMMENT,
BLOCK_COMMENT,
DOC_COMMENT
}
/**
* Information extracted about a source code entity.
*/
public class SourceCodeEntity {
public SCEType type;
public SCEHow how;
public String name;
public int totalLength;
public int startLine, endLine;
public int startCol, endCol;
private int startModelOffset;
public String getName(){
if( type != SCEType.COMMENT )
return name;
else{
int widgetOffsetStart = projectionViewer.modelOffset2WidgetOffset(startModelOffset);
int widgetOffsetEnd = widgetOffsetStart+totalLength;
if( widgetOffsetStart >= 0 && widgetOffsetEnd >= 0)
return styledText.getText(widgetOffsetStart,widgetOffsetEnd);
else{
int offsetStart = startModelOffset;
while(widgetOffsetStart < 0){
offsetStart++;
widgetOffsetStart = projectionViewer.modelOffset2WidgetOffset(offsetStart);
}
int shownLineIndex = styledText.getLineAtOffset(offsetStart);
return styledText.getLine(shownLineIndex);
}
}
}
}
/**
* Task ran on UI thread to reload the AST. Like any UIJob, it can be
* scheduled to occur later and can be canceled.
*/
private class ReloadAstJob extends UIJob {
private AstManager astManager;
public ReloadAstJob(String name, AstManager astManager) {
super(name);
this.astManager = astManager;
}
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
astManager.reload();
return Status.OK_STATUS;
}
}
final private int AFTER_KEYPRESS_RELOAD_THRESHOLD_MILLIS = 1000;
private IEditorPart editor;
private StyledText styledText;
private ProjectionViewer projectionViewer;
private ReloadAstJob reloadAstJob;
private LinkedList<SourceCodeEntity> sourceCodeEntities;
private String editorPath;
/**
* Constructor. Loads the AST and sets up the StyledText to automatically
* reload after certain events.
* @param editor IEditorPart which owns the following StyledText.
* @param styledText StyledText to which this AST pertains.
*/
public AstManager(IEditorPart editor, StyledText styledText) {
try {
editorPath = ((IFileEditorInput) editor.getEditorInput()).getFile()
.getFullPath().toFile().getCanonicalPath();
} catch (IOException e) {
// ignore IOErrors while constructing path
editorPath = "?";
}
this.editor = editor;
this.styledText = styledText;
//This is the only why I know to get the ProjectionViewer. Perhaps there is better way. ~Ben
ITextOperationTarget t = (ITextOperationTarget) editor.getAdapter(ITextOperationTarget.class);
if(t instanceof ProjectionViewer) projectionViewer = (ProjectionViewer)t;
hookupAutoReload();
reload();
}
/**
* Returns a string representation of the path to the
* file associated with the current editor.
*/
public String getPath() {
return editorPath;
}
public ProjectionViewer getProjectionViewer(){
return projectionViewer;
}
/**
* Gets the source code entities found at a location in source.
* @param lineNumber 1-based line number.
* @param colNumber 0-based column number.
* @return Array of all source code entities found.
*/
public SourceCodeEntity[] getSCEs(int lineNumber, int colNumber) {
LinkedList<SourceCodeEntity> entities =
new LinkedList<SourceCodeEntity>();
//Look through source code entities to find all entities at the given
//location. They are already sorted from most specific to least
//specific.
for (SourceCodeEntity sce : sourceCodeEntities) {
boolean found = true;
if (lineNumber < sce.startLine || lineNumber > sce.endLine)
found = false;
if (lineNumber == sce.startLine && colNumber < sce.startCol)
found = false;
if (lineNumber == sce.endLine && colNumber > sce.endCol)
found = false;
if (found)
entities.add(sce);
}
return entities.toArray(new SourceCodeEntity[0]);
}
/**
* Reloads the AST from the current contents of the StyledText.
*/
public void reload() {
//Reset source code entities list.
sourceCodeEntities = new LinkedList<SourceCodeEntity>();
IFile file = ((IFileEditorInput) editor.getEditorInput()).getFile();
IProject project = file.getProject();
IJavaProject jProject = JavaCore.create(project);
ICompilationUnit compUnit = (ICompilationUnit) JavaCore.create(file);
//If the compilation unit is null, this is not a Java file.
if (compUnit == null)
return;
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setProject(jProject);
parser.setSource(compUnit);
parser.setResolveBindings(true);
parser.setUnitName(file.getName());
final CompilationUnit compileUnit =
(CompilationUnit) parser.createAST(null);
ASTVisitor visitor = new ASTVisitor() {
public boolean visit(TypeDeclaration node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.TYPE;
sce.how = SCEHow.DECLARE;
ITypeBinding binding = node.resolveBinding();
if (binding != null)
sce.name = binding.getQualifiedName();
else
sce.name = "?." + node.getName().getFullyQualifiedName();
determineSCEPosition(compileUnit, node, sce);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(MethodDeclaration node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.METHOD;
sce.how = SCEHow.DECLARE;
sce.name = node.getName().getFullyQualifiedName();
determineSCEPosition(compileUnit, node, sce);
extractDataFromMethodBinding(node.resolveBinding(), sce);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(VariableDeclarationFragment node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.VARIABLE;
sce.how = SCEHow.DECLARE;
sce.name = node.getName().getFullyQualifiedName();
determineSCEPosition(compileUnit, node, sce);
extractDataFromVariableBinding(node.resolveBinding(), sce);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(EnumDeclaration node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.ENUM;
sce.how = SCEHow.DECLARE;
sce.name = node.getName().getFullyQualifiedName();
determineSCEPosition(compileUnit, node, sce);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(MethodInvocation node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.METHOD;
sce.how = SCEHow.USE;
sce.name = node.getName().getFullyQualifiedName();
determineSCEPosition(compileUnit, node, sce);
extractDataFromMethodBinding(node.resolveMethodBinding(), sce);
sourceCodeEntities.add(sce);
return false;
}
public boolean visit(FieldAccess node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.VARIABLE;
sce.how = SCEHow.USE;
sce.name = node.getName().getFullyQualifiedName();
determineSCEPosition(compileUnit, node, sce);
extractDataFromVariableBinding(node.resolveFieldBinding(), sce);
sourceCodeEntities.add(sce);
return false;
}
public boolean visit(ImportDeclaration node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.IMPORT;
sce.how = SCEHow.DECLARE;
sce.name = node.getName().getFullyQualifiedName();
determineSCEPosition(compileUnit, node, sce);
sourceCodeEntities.add(sce);
return false;
}
public boolean visit(ForStatement node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.FORSTATEMENT;
sce.how = SCEHow.DECLARE;
determineSCEPosition(compileUnit, node, sce);
sce.name = String.format("ForStatement-l%dc%d",
sce.startLine, sce.startCol);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(EnhancedForStatement node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.FORSTATEMENT;
sce.how = SCEHow.DECLARE;
determineSCEPosition(compileUnit, node, sce);
sce.name = String.format("EnhancedForStatement-l%dc%d",
sce.startLine, sce.startCol);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(WhileStatement node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.WHILESTATEMENT;
sce.how = SCEHow.DECLARE;
determineSCEPosition(compileUnit, node, sce);
sce.name = String.format("WhileStatement-l%dc%d",
sce.startLine, sce.startCol);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(SwitchStatement node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.SWITCHSTATEMENT;
sce.how = SCEHow.DECLARE;
determineSCEPosition(compileUnit, node, sce);
sce.name = String.format("SwitchStatement-l%dc%d",
sce.startLine, sce.startCol);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(IfStatement node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.IFSTATEMENT;
sce.how = SCEHow.DECLARE;
determineSCEPosition(compileUnit, node, sce);
sce.name = String.format("IfStatement-l%dc%d",
sce.startLine, sce.startCol);
sourceCodeEntities.add(sce);
return true;
}
public boolean visit(ConditionalExpression node) {
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.CONDITIONAL_EXPRESSION;
sce.how = SCEHow.DECLARE;
determineSCEPosition(compileUnit, node, sce);
sce.name = String.format("ConditionalExpression-l%dc%d",
sce.startLine, sce.startCol);
sourceCodeEntities.add(sce);
return true;
}
};
compileUnit.accept(visitor);
//Get comments separately.
for (Object comment_obj : compileUnit.getCommentList()) {
if (comment_obj instanceof Comment) {
Comment comment = (Comment) comment_obj;
SourceCodeEntity sce = new SourceCodeEntity();
sce.type = SCEType.COMMENT;
if(comment.isLineComment()) sce.how = SCEHow.LINE_COMMENT;
else if(comment.isBlockComment()) sce.how = SCEHow.BLOCK_COMMENT;
else sce.how = SCEHow.DOC_COMMENT;
//The projectionViewer is used to convert the ASTNode's model offset to a Widget offset. ~Ben
sce.startModelOffset = comment.getStartPosition();
sce.totalLength = comment.getLength();
int widgetOffsetStart = projectionViewer.modelOffset2WidgetOffset(comment.getStartPosition());
int widgetOffsetEnd = widgetOffsetStart+comment.getLength();
if( widgetOffsetStart >= 0 && widgetOffsetEnd >= 0)
sce.name = styledText.getText(widgetOffsetStart,widgetOffsetEnd);
else{
int offsetStart = comment.getStartPosition();
while(widgetOffsetStart < 0){
offsetStart++;
widgetOffsetStart = projectionViewer.modelOffset2WidgetOffset(offsetStart);
}
int shownLineIndex = styledText.getLineAtOffset(offsetStart);
sce.name = styledText.getLine(shownLineIndex);
}
determineSCEPosition(compileUnit, comment, sce);
sourceCodeEntities.add(sce);
}
}
//Smaller entities take higher priority. If a method appears in a class,
//for example, a query for the method should return the method instead
//of the class.
Collections.sort(sourceCodeEntities,
new Comparator<SourceCodeEntity>() {
@Override
public int compare(SourceCodeEntity lhs, SourceCodeEntity rhs) {
return lhs.totalLength - rhs.totalLength;
}
});
}
/**
* Get line/column start/end information about an ASTNode.
* @param compileUnit Compilation unit to which the ASTNode belongs.
* @param node The ASTNode.
* @param sce The SourceCodeEntity in which to store the result.
*/
private static void determineSCEPosition(CompilationUnit compileUnit,
ASTNode node, SourceCodeEntity sce) {
sce.totalLength = node.getLength();
sce.startLine = compileUnit.getLineNumber(node.getStartPosition());
sce.endLine = compileUnit.getLineNumber(node.getStartPosition() +
node.getLength());
sce.startCol = compileUnit.getColumnNumber(node.getStartPosition());
sce.endCol = compileUnit.getColumnNumber(node.getStartPosition() +
node.getLength());
}
/**
* Extracts the fully qualified name and parameters from a method binding
* and applies them to the name in a SourceCodeEntity.
* @param binding The method binding.
* @param sce SourceCodeEntity to which to apply changes. Name must be set
* to the entity's unqualified name.
*/
private void extractDataFromMethodBinding(IMethodBinding binding,
SourceCodeEntity sce) {
if (binding != null) {
//Get package and type name within which this method is declared.
ITypeBinding type = binding.getDeclaringClass();
if (type != null)
sce.name = type.getQualifiedName() + "." + sce.name;
else
sce.name = "?." + sce.name;
//Get method parameter types
String params = "";
for (ITypeBinding paramType : binding.getParameterTypes()) {
if (paramType != null)
params += paramType.getQualifiedName() + ",";
}
if (params.length() > 0) {
sce.name += "("
+ params.substring(0, params.length() - 1)
+ ")";
} else
sce.name += "()";
} else {
//If binding fails, mark the qualification as "?" to show it could
//not be determined.
sce.name = "?." + sce.name + "(?)";
}
}
/**
* Extracts the fully qualified name from a variable binding and applies
* them to the name in a SourceCodeEntity.
* @param binding The variable binding.
* @param sce SourceCodeEntity to which to apply changes. Name must be set
* to the entity's unqualified name.
*/
private static void extractDataFromVariableBinding(
IVariableBinding binding, SourceCodeEntity sce) {
if (binding != null) {
//Type member variable.
ITypeBinding type = binding.getDeclaringClass();
if (type != null)
sce.name = type.getQualifiedName() + "." + sce.name;
//Variable declared in method.
else {
IMethodBinding method = binding.getDeclaringMethod();
if (method != null) {
type = method.getDeclaringClass();
if (type != null) {
sce.name = type.getQualifiedName() + "."
+ method.getName() + "." + sce.name;
} else
sce.name = "?." + method.getName() + "." + sce.name;
} else
sce.name = "?." + sce.name;
}
} else {
//If binding fails, mark the qualification as "?" to show it could
//not be determined.
sce.name = "?." + sce.name;
}
}
/**
* Called by constructor. Hooks up the StyledText with listeners to reload
* the AST when it is likely to have changed.
*/
private void hookupAutoReload() {
final AstManager astManager = this;
//Listen for key activity, then reload when inactivity follows.
KeyListener keyListener = new KeyListener() {
@Override
public void keyPressed(KeyEvent e) {
//Do nothing.
}
@Override
public void keyReleased(KeyEvent e) {
if (reloadAstJob != null)
reloadAstJob.cancel();
reloadAstJob = new ReloadAstJob("reloadAstJob", astManager);
reloadAstJob.schedule(AFTER_KEYPRESS_RELOAD_THRESHOLD_MILLIS);
}
};
styledText.addKeyListener(keyListener);
}
}